<?php

declare(strict_types=1);

namespace core;

/**
 * 全局错误与异常处理器（单例模式）
 *
 * 负责接管 PHP 错误、未捕获异常以及致命错误，
 * 并根据是否启用调试模式输出不同的响应。
 */
final class ErrorHandler
{
    /**
     * 是否启用调试模式
     *  @var bool
     */
    private bool $debug = false;

    /**
     * 单例实例
     * @var self|null
     */
    private static ?self $instance = null;

    /**
     * 私有构造函数，防止外部实例化
     */
    private function __construct()
    {
    }

    /**
     * 获取单例实例
     *
     * @return self
     */
    public static function getInstance(): self
    {
        return self::$instance ??= new self();
    }

    /**
     * 注册错误和异常处理程序
     *
     *  @return self 返回当前实例以支持链式调用
     */
    public function register(): self
    {
        error_reporting(E_ALL);

        set_error_handler([$this, 'handleError']);
        set_exception_handler([$this, 'handleException']);
        register_shutdown_function([$this, 'handleShutdown']);

        // 统一由本类控制输出，关闭直接显示错误
        @ini_set('display_errors', '0');

        return $this;
    }

    /**
     * 获取当前调试模式状态
     *
     * @return bool
     */
    public function isDebug(): bool
    {
        return $this->debug;
    }

    /**
     * 设置调试模式（可链式调用）
     *
     * @param bool $debug
     * @return self 返回当前实例以支持链式调用
     */
    public function setDebug(bool $debug): self
    {
        $this->debug = $debug;
        return $this;
    }

    /**
     * 错误转异常处理器
     *
     * @param int    $errno   错误级别
     * @param string $errstr  错误消息
     * @param string $errfile 出错文件
     * @param int    $errline 出错行号
     * @return bool
     * @throws \ErrorException
     */
    public function handleError($errno, $errstr, $errfile, $errline): bool
    {
        // 被 @ 屏蔽或不在报告级别内则交还 PHP 处理
        if (!(error_reporting() & $errno)) {
            return false;
        }

        throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
    }

    /**
     * 未捕获异常处理器
     *
     * @param \Throwable $e
     * @return void
     */
    public function handleException(\Throwable $e): void
    {
        $this->renderResponse($e);
    }

    /**
     * 致命错误处理器
     *
     * @return void
     */
    public function handleShutdown(): void
    {
        $error = error_get_last();
        $fatalTypes = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];

        if ($error && in_array($error['type'], $fatalTypes, true)) {
            // 仅当有致命错误时才清理输出缓冲
            while (ob_get_level()) {
                if (!ob_end_clean()) {
                    break; // 防止无限循环
                }
            }

            $e = new \ErrorException(
                $error['message'],
                0,
                $error['type'],
                $error['file'],
                $error['line']
            );
            $this->renderResponse($e);
        }
    }

    /**
     * 根据运行模式渲染错误响应
     *
     * @param \Throwable $e
     * @return void
     */
    private function renderResponse(\Throwable $e): void
    {
        http_response_code(500);

        if ($this->debug) {
            $this->renderDebugResponse($e);
        } else {
            $this->renderProductionResponse($e);
        }

        $this->shutdown();
    }

    /**
     * 可被重写的退出行为（用于测试）
     *
     * @return void
     */
    protected function shutdown(): void
    {
        exit(1);
    }

    /**
     * 调试模式输出详细错误
     *
     * @param \Throwable $e
     * @return void
     */
    private function renderDebugResponse(\Throwable $e): void
    {
        echo '<h1>Internal Server Error</h1>';
        echo '<p><strong>Message:</strong> ' . $this->escape($e->getMessage()) . '</p>';
        echo '<p><strong>File:</strong> ' . $this->escape($e->getFile()) . ' on line <strong>' . $e->getLine() . '</strong></p>';

        $this->renderCodeSnippet($e->getFile(), $e->getLine());

        echo '<h2>Stack Trace</h2>';
        echo '<pre>' . $this->escape($e->getTraceAsString()) . '</pre>';
    }

    /**
     * 生产模式输出友好提示
     *
     * @param \Throwable $e
     * @return void
     */
    private function renderProductionResponse(\Throwable $e): void
    {
        echo '<h1>应用程序错误</h1>';
        echo '<p>发生意外错误，请稍后再试。</p>';
    }

    /**
     * 显示错误行的前后几行代码
     *
     * @param string $file 文件路径
     * @param int    $errorLine 出错行号
     * @param int    $padding 上下文行数
     * @return void
     */
    private function renderCodeSnippet(string $file, int $errorLine, int $padding = 5): void
    {
        // 安全检查
        if (!is_readable($file)) {
            echo '<p><b>源文件不可读。</b></p>';
            return;
        }

        // 限制文件大小（防止大文件加载）
        if (filesize($file) > 1024 * 1024) { // 1MB
            echo '<p><b>源文件太大，无法显示。</b></p>';
            return;
        }

        $lines = @file($file, FILE_IGNORE_NEW_LINES);
        if ($lines === false) {
            echo '<p><b>读取文件失败。</b></p>';
            return;
        }

        $totalLines = count($lines);
        $start      = max($errorLine - $padding - 1, 0);
        $end        = min($errorLine + $padding - 1, $totalLines - 1);

        echo '<h2>Code Snippet (around line ' . $errorLine . ')</h2>';
        echo '<pre style="background:#f8f8f8; padding:10px; border:1px solid #ccc; overflow:auto; max-height:400px;">';

        for ($i = $start; $i <= $end; $i++) {
            $lineNumber = $i + 1;
            $code = $this->escape($lines[$i]);

            if ($lineNumber === $errorLine) {
                echo '<span style="background:#fee; color:#900; display:block;">'
                    . str_pad((string)$lineNumber, 6, ' ', STR_PAD_LEFT)
                    . ': ' . $code . "</span>\n";
            } else {
                echo str_pad((string)$lineNumber, 6, ' ', STR_PAD_LEFT)
                    . ': ' . $code . "\n";
            }
        }

        echo '</pre>';
    }

    /**
     * 安全转义字符串
     *
     * @param string $str
     * @return string
     */
    private function escape(string $str): string
    {
        return htmlspecialchars($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    }

    // 防止克隆
    private function __clone()
    {
    }

    /**
     * 防止反序列化单例对象
     *
     * @return void
     * @throws \RuntimeException
     */
    public function __wakeup(): void
    {
        throw new \RuntimeException("Cannot unserialize singleton");
    }
}
